package com.ouyunc.oauth2.config;


import com.ouyunc.cache.redis.RedisFactory;
import com.ouyunc.common.constant.Auth2Constant;
import com.ouyunc.common.context.SpringContextHolder;
import com.ouyunc.oauth2.config.override.*;
import com.ouyunc.oauth2.exception.IOAuth2WebResponseExceptionTranslator;
import com.ouyunc.oauth2.feign.UserClient;
import com.ouyunc.oauth2.service.IUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.GenericTypeResolver;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.ObjectPostProcessor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService;
import org.springframework.security.oauth2.provider.code.AuthorizationCodeServices;
import org.springframework.security.oauth2.provider.code.RandomValueAuthorizationCodeServices;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider;

import javax.annotation.Resource;
import javax.servlet.Filter;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.*;
import java.util.concurrent.TimeUnit;

/**
 * @author fangzhenxun
 * @date 2019/11/6 10:39
 * @description 认证授权服务器配置,负责用户登录、授权、token验证等。 采用采用jwt认证
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {




    /**
     * token 的动态 redis 前缀
     **/
    @Autowired
    private TokenDynamicRedisPrefixProperties tokenDynamicRedisPrefixProperties;



    /**
     * 密码加密
     **/
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 认证管理很重要 如果security版本高可能会出坑哦
     * 注入authenticationManager 来支持 password grant type
     **/
    @Resource
    private AuthenticationManager authenticationManager;

    /**
     *  用户信息数据, 自定义的用户详情
     **/
    @Autowired
    private IUserDetailsService iuserDetailsService;



    /**
     *  用户服务客户端
     **/
    @Autowired
    private UserClient userClient;

    /**
     *  自定义oauth2异常转换器，返回统一格式xytResponse
     **/
    @Autowired
    private IOAuth2WebResponseExceptionTranslator oauth2WebResponseExceptionTranslator;

    /**
     *  jwt 签名
     **/
    private String signingKey = "fzx";

    /**
     * @Author fangzhenxun
     * @Description  允许表单验证，浏览器直接发送post请求即可获取tocken
     *               用来定义令牌端点上的安全约束
     * @Date  2019/11/6 11:00
     * @Param [security]
     * @return void
     **/
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security){

        security.tokenKeyAccess("permitAll()")

                //开启token校验端点的认证权限访问，---》url:/oauth/check_token allow check token
                .checkTokenAccess("isAuthenticated()")
                //开放表单登录,自定义了,实际也是开启了由clientCredentialsTokenEndpointFilter替换
                //.allowFormAuthenticationForClients()
                // 自定义客户端过滤器，替换客户端认证过滤器
                .addTokenEndpointAuthenticationFilter(tokenEndpointAuthenticationFilter ());
    }

    /**
     * 自定义客户端认证过滤器替代原有的
     * @return
     */
    private Filter tokenEndpointAuthenticationFilter () {
        ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter("/oauth/token");
        clientCredentialsTokenEndpointFilter.setAuthenticationManager(authentication -> {
            // 使用自定义的去做认证
            IClientDetailsAuthenticationProvider iClientDetailsAuthenticationProvider = new IClientDetailsAuthenticationProvider(passwordEncoder, new ClientDetailsUserDetailsService(clientDetails()));
            iClientDetailsAuthenticationProvider.setHideUserNotFoundExceptions(false);
            return iClientDetailsAuthenticationProvider.authenticate(authentication);
        });
        IOAuth2AuthenticationEntryPoint ioAuth2AuthenticationEntryPoint = new IOAuth2AuthenticationEntryPoint();
        ioAuth2AuthenticationEntryPoint.setTypeName("Form");
        ioAuth2AuthenticationEntryPoint.setRealmName("oauth2/client");
        clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(ioAuth2AuthenticationEntryPoint);
        // 具体实现请看SecurityConfigurerAdapter 类的postProcess() 方法
        ObjectPostProcessor opp = SpringContextHolder.getBean(ObjectPostProcessor.class);
        Class<?> oppClass = opp.getClass();
        Class<?> oppType = GenericTypeResolver.resolveTypeArgument(oppClass, ObjectPostProcessor.class);
        if (oppType == null || oppType.isAssignableFrom(clientCredentialsTokenEndpointFilter.getClass())) {
            clientCredentialsTokenEndpointFilter = (ClientCredentialsTokenEndpointFilter) opp.postProcess(clientCredentialsTokenEndpointFilter);
        }
        return clientCredentialsTokenEndpointFilter;
    }

    /** 第一个config配置-----》 客户端配置
     * @Author fangzhenxun
     * @Description  说明：这个方法主要是用于校验注册的第三方客户端的信息，可以存储在数据库中，默认方式是存储在内存中，如下所示，注释掉的代码即为内存中存储的方式
     *                  1，用来配置客户端详情服务（ClientDetailsService），客户端详情信息在这里进行初始化，
     *                  2，你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息。
     * @Date  2019/11/6 11:06
     * @Param [clients]
     * @return void
     **/
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        //配置获取客户端信息的方式
        clients.withClientDetails(clientDetails());
    }



    /** 第二个配置---》端点信息配置
     * @Author fangzhenxun
     * @Description 说明：这个方法主要的作用用于控制token的端点等信息
     *               1，定义授权和令牌端点以及令牌服务, 下面使用jwt token 来存储信息，也可以放在redis中，
     * @Date  2019/11/6 11:07
     * @Param [endpoints]
     * @return void
     **/
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        //jwt token增强
        TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
        List<TokenEnhancer> enhancerList = new ArrayList<>();
        enhancerList.add(jwtTokenEnhancer());
        enhancerList.add(jwtAccessTokenConverter());
        enhancerChain.setTokenEnhancers(enhancerList);


        //自定义tokenService, 使用自定义的UserDetailsService
        ITokenServices tokenServices = new ITokenServices();
        tokenServices.setTokenStore(tokenStore());
        tokenServices.setSupportRefreshToken(true);
        //这里的重复使用刷新token起作用（原因是使用了自定义的tokenServices），建议和下面的endpoints中设置的保持一致
        tokenServices.setReuseRefreshToken(false);
        tokenServices.setClientDetailsService(clientDetails());
        tokenServices.setTokenEnhancer(enhancerChain);

        // 设置自定义的IUserDetailsByNameServiceWrapper，刷新token使用
        PreAuthenticatedAuthenticationProvider provider = new PreAuthenticatedAuthenticationProvider();
        provider.setPreAuthenticatedUserDetailsService(new IUserDetailsByNameServiceWrapper(iuserDetailsService));
        tokenServices.setAuthenticationManager(new ProviderManager(Arrays.asList(provider)));

        //添加认证管理器
        endpoints.authenticationManager(authenticationManager)
                //token 存储,如果不设置自定义的tokenServices 则底层默认是DefaultTokenServices 且tokenStore就是这里的tokenStore()方法，当然自定义了tokenServices,并且设置了tokenStore，那么这里不指定也可以（暂时没发现问题）
                .tokenStore(tokenStore())
                //配置用户信息数据 ,需要加上否则刷新token 时会报错
                .userDetailsService(iuserDetailsService)
                //这里使用jwt,配置token转换
                .accessTokenConverter(jwtAccessTokenConverter())
                //是否允许重复使用refresh token(refresh_token 还是会刷新改变，但是里面的内容则大部分不会改变ati 会变指向新的access_token),这里不允许重复使用
//            注意：
//           （1）当刷新token的时候如果在认证服务其设置重复使用refresh_token（即reuseRefreshTokens(true) 默认值）时 ；
//               (1.1)在access_token没有过期时调用刷新token: ，
//                    生成的新access_token（有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了）
//                    生成新的refresh_token（有效时间不会重新计时），但是刷新后生成的新refresh_token 中的jti的值和之前老的的refresh_token 的jti的值是相同的（所以可以理解refresh_token没有改变，既可以重复使用原始的refresh_token），但是refresh_token字符串确实返回新的但是并没有在redis中存储，所以只能使用第一次（原始生成的refresh_token 来进行以后的刷新，如果使用后来生成的refresh_token来刷新则会报错）的refresh_token来继续以后的刷新，直到refresh_token过期
//               (1.2)在access_token过期但refresh_token没有过期时调用刷新token:
//                   生成的新access_token（有效时间会重新计时）
//                   生成新的refresh_token（有效时间不会重新计时），但是刷新后生成的新refresh_token 中的jti的值和之前老的的refresh_token 的jti的值是相同的（所以可以理解refresh_token没有改变，既可以重复使用原始的refresh_token），但是refresh_token字符串确实返回新的但是并没有在redis中存储，所以只能使用第一次（原始生成的refresh_token 来进行以后的刷新，如果使用后来生成的refresh_token来刷新则会报错）的refresh_token来继续以后的刷新，直到refresh_token过期
//               （1.3）在refresh_token过期的时候刷新，直接抛出异常
//            （2）当刷新token的时候如果在认证服务其设置重复使用refresh_token（即reuseRefreshTokens(false）时 ；
//                (2.1)在access_token没有过期时调用刷新token: ，
//                   生成的新access_token（有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了），
//                   生成的refresh_token （有效时间会重新计时） 而老的old access_token(无论在不在有效期内)都不可以使用（已经被删掉了）, 如果再次刷新则使用新生成的refresh_token
//                (2.2)在access_token过期但refresh_token没有过期时调用刷新token:
//                   （2.2.1） TODO 这个问题需要解决：（默认情况下）如果先调用生成access_token接口，针对该用户会生成一套新的相关token（也包括新的刷新token）并且未失效的refresh_token还在生效，如果此时使用原来的老的refresh_token再次调用刷新token接口则会再次生成一套有效token，这个时候就会出现一个用户有两套有效的token可以使用，
//                             解决方法：可以参考在调用生成token（登录）接口的时候先调用删除token接口（这里指的是删除刷新token的接口）
//                   （2.2.1）直接调用刷新token接口，则之前的refresh_token会被覆盖重新生成与计时
//                            生成的新access_token（有效时间会重新计时）
//                            生成的refresh_token （有效时间会重新计时） 而老的old access_token 已经被删除
//                （2.3）在refresh_token过期的时候刷新，直接抛出异常
                //上面的在tokenServices中定义的reuseRefreshTokens起作用（因为不是走的默认DefaultTokenServices），建议tokenServices中的reuseRefreshTokens和这里的一起修改成一致
                .reuseRefreshTokens(false)
                //使用自定义的tokenServices,在解决自定义userDetailsService刷新token的问题,如果不设置会走DefaultTokenServices
                .tokenServices(tokenServices)
                //设置token 扩展增强器
                .tokenEnhancer(enhancerChain)
                //处理 ExceptionTranslationFilter 抛出的异常(也就是自定义oauth2异常)为了方便统一放回响应格式
                .exceptionTranslator(oauth2WebResponseExceptionTranslator)
                // 具体入口类DefaultRedirectResolver, 异常在AuthorizationEndpoint类中抛出捕获； 自定义授权码验证时确认页面， 最后一个参数为替换之后页面的url
                // 注意 自定义页面的授权是根据oauth_client_details 表中的scope 进行授权的，如果scope值存在多个就会列出多个，具体看你怎么实现
                .pathMapping("/oauth/confirm_access", Auth2Constant.OAUTH2_CONFIRM_ACCESS)
                // 具体入口类DefaultRedirectResolver  异常在AuthorizationEndpoint类中抛出捕获； 自定义授权码,如果重定向路径错误也重定向登录页面，并且提示信息，当然你也可以重定向其他页面
                .pathMapping("/oauth/error", Auth2Constant.OAUTH2_ERROR)
                //配置授权码模式下code的存储方式
                .authorizationCodeServices(authorizationCodeServices());

    }



    /**
     * @Author fangzhenxun
     * @Description 声明clientDetails 的实现：注意：可以配置两种方式，这里使用第二种加入缓存
     * 1，直接取出clients 信息从数据库，
     * 2，缓存 + 数据库（需要继承 JdbcClientDetailsService 然后重写里面的方法），
     * @Date  2019/11/6 13:17
     * @Param
     * @return
     **/
    @Bean
    public ClientDetailsService clientDetails() {
        //jdbc数据库方式 存取客户端信息,加入缓存
        return new IJdbcClientDetailsService(RedisFactory.redisTemplate(), userClient);
    }



    /**
     * @Author fangzhenxun
     * @Description  用户验证信息的保存策略，可以存储在内存中，关系型数据库中，redis中；当前使用jwt tokenStore 存储
     * @Date  2019/11/6 14:04
     * @Param [] 这里修改成IRedisTokenStore TokenStore
     * @return org.springframework.security.oauth2.provider.token.TokenStore
     **/
    @Bean
    public IRedisTokenStore tokenStore() {
        //redis 存储token
        IRedisTokenStore iRedisTokenStore = new IRedisTokenStore(RedisFactory.redisTemplate().getConnectionFactory());
        iRedisTokenStore.setPrefixMap(tokenDynamicRedisPrefixProperties.getRedisPrefix());
        // 这里可以更改默认的token zai redis 存储的前缀
        return iRedisTokenStore;
    }




    /**
     * @Author fangzhenxun
     * @Description   如果使用jwt作为storeToken可以配置该实例做扩展， jwt 扩展增强
     * @Date 2020/4/15 11:05
     * @param
     * @return org.springframework.security.oauth2.provider.token.TokenEnhancer
     **/
    @Bean
    public TokenEnhancer jwtTokenEnhancer(){
        return (accessToken, authentication) -> {
            Map<String,Object> info = new HashMap<>(1);
            //token创建时间（毫秒）
            info.put("create_time", LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli());
            //将用户信息使用AES对称加密后返回，防止他人拿到jwt直接解密获取敏感数据
            info.put("user_name", authentication.getPrincipal());
            //设置附加信息
            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
            return accessToken;
        };
    }



    /**
     * @Author fangzhenxun
     * @Description Jwt资源令牌转换器，用来配置加密解密, 当使用jwt 存储的时候使用,
     * @Date  2019/11/6 14:13
     * @Param []对称加密算法 https://blog.csdn.net/nielinqi520/article/details/80167042
     * @return org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter
     **/
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
        // 这里务必设置一个，否则多台认证中心的话，一旦使用jwt方式，access_token将解析错误
        jwtAccessTokenConverter.setSigningKey(signingKey);
        return jwtAccessTokenConverter;
    }


    /**
     * @Author fangzhenxun
     * @Description 注册一个AuthorizationCodeServices以保存authorization_code的授权码code
     * 生成一个RandomValueAuthorizationCodeServices的bean，
     * 而不是直接生成AuthorizationCodeServices的bean。
     * RandomValueAuthorizationCodeServices可以帮我们完成code的生成过程。
     * 如果你想按照自己的规则生成授权码code请直接生成AuthorizationCodeServices的bean
     * @Date 2020/7/10 17:14
     * @return org.springframework.security.oauth2.provider.code.AuthorizationCodeServices
     **/
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        RedisTemplate<String, OAuth2Authentication> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(RedisFactory.redisTemplate().getConnectionFactory());
        redisTemplate.afterPropertiesSet();
        /**
         * 只需要重写这两个方法就可以了，生成code规则还交给台来生成
         **/
        return new RandomValueAuthorizationCodeServices() {

            /**
             * code存储规则
             **/
            @Override
            protected void store(String code, OAuth2Authentication authentication) {
                //10分钟过期
                redisTemplate.boundValueOps(code).set(authentication, 10, TimeUnit.MINUTES);
            }

            /**
             * code验证并删除
             **/
            @Override
            protected OAuth2Authentication remove(String code) {
                OAuth2Authentication authentication =  redisTemplate.boundValueOps(code).get();
                redisTemplate.delete(code);
                return authentication;
            }
        };
    }
}
